| 1 | import { Stack, router, useLocalSearchParams } from "expo-router"; |
| 2 | import { CircleDot, GitPullRequest } from "lucide-react-native"; |
| 3 | import { useState } from "react"; |
| 4 | import { FlatList, RefreshControl, View } from "react-native"; |
| 5 | |
| 6 | import { Checks, IssueIcon, PullIcon } from "@/components/code"; |
| 7 | import { Segments } from "@/components/segments"; |
| 8 | import { Empty, ErrorView, Loading, Row, Screen, Text } from "@/components/ui"; |
| 9 | import { type ApiIssue, type ApiPull, useIssues, usePulls, useRepo } from "@/lib/code"; |
| 10 | import { ago } from "@/lib/time"; |
| 11 | import { space, useColors } from "@/theme"; |
| 12 | |
| 13 | type Tab = "pulls" | "issues"; |
| 14 | type State = "open" | "closed"; |
| 15 | |
| 16 | /** A repository: its pull requests and issues. */ |
| 17 | export default function Repository() { |
| 18 | const { owner, repo } = useLocalSearchParams<{ owner: string; repo: string }>(); |
| 19 | const [tab, setTab] = useState<Tab>("pulls"); |
| 20 | const [state, setState] = useState<State>("open"); |
| 21 | const details = useRepo(owner, repo); |
| 22 | const pulls = usePulls(owner, repo, state); |
| 23 | const issues = useIssues(owner, repo, state); |
| 24 | const list = tab === "pulls" ? pulls : issues; |
| 25 | const colors = useColors(); |
| 26 | |
| 27 | return ( |
| 28 | <Screen> |
| 29 | <Stack.Screen options={{ title: repo }} /> |
| 30 | <View style={{ padding: space.lg, gap: space.md }}> |
| 31 | <Text size="small" tone="muted"> |
| 32 | {owner}/{repo} |
| 33 | {details.data?.description ? ` · ${details.data.description}` : ""} |
| 34 | </Text> |
| 35 | <Segments |
| 36 | value={tab} |
| 37 | onChange={setTab} |
| 38 | options={[ |
| 39 | { value: "pulls", label: "Pull requests" }, |
| 40 | { value: "issues", label: "Issues" }, |
| 41 | ]} |
| 42 | /> |
| 43 | <Segments |
| 44 | value={state} |
| 45 | onChange={setState} |
| 46 | options={[ |
| 47 | { value: "open", label: "Open" }, |
| 48 | { value: "closed", label: "Closed" }, |
| 49 | ]} |
| 50 | /> |
| 51 | </View> |
| 52 | {list.isPending ? ( |
| 53 | <Loading /> |
| 54 | ) : list.error ? ( |
| 55 | <ErrorView error={list.error} retry={() => list.refetch()} /> |
| 56 | ) : tab === "pulls" ? ( |
| 57 | <FlatList |
| 58 | data={pulls.data} |
| 59 | keyExtractor={(pull) => pull.id} |
| 60 | refreshControl={<RefreshControl refreshing={pulls.isRefetching} onRefresh={() => pulls.refetch()} tintColor={colors.accent} />} |
| 61 | ListEmptyComponent={<Empty icon={GitPullRequest} title={`No ${state} pull requests`} />} |
| 62 | renderItem={({ item }) => <PullRow pull={item} owner={owner} repo={repo} />} |
| 63 | /> |
| 64 | ) : ( |
| 65 | <FlatList |
| 66 | data={issues.data} |
| 67 | keyExtractor={(issue) => issue.id} |
| 68 | refreshControl={<RefreshControl refreshing={issues.isRefetching} onRefresh={() => issues.refetch()} tintColor={colors.accent} />} |
| 69 | ListEmptyComponent={<Empty icon={CircleDot} title={`No ${state} issues`} />} |
| 70 | renderItem={({ item }) => <IssueRow issue={item} owner={owner} repo={repo} />} |
| 71 | /> |
| 72 | )} |
| 73 | </Screen> |
| 74 | ); |
| 75 | } |
| 76 | |
| 77 | function PullRow({ pull, owner, repo }: { pull: ApiPull; owner: string; repo: string }) { |
| 78 | const by = pull.author.display_username ?? pull.author.username; |
| 79 | return ( |
| 80 | <Row |
| 81 | leading={<PullIcon pull={pull} />} |
| 82 | title={pull.title} |
| 83 | subtitle={`#${pull.number} · ${by}${pull.agent ? ` with ${pull.agent}` : ""} · ${ago(pull.updated_at)}`} |
| 84 | trailing={<Checks status={pull.check_status} />} |
| 85 | onPress={() => router.push({ pathname: "/code/[owner]/[repo]/pull/[number]", params: { owner, repo, number: String(pull.number) } })} |
| 86 | /> |
| 87 | ); |
| 88 | } |
| 89 | |
| 90 | function IssueRow({ issue, owner, repo }: { issue: ApiIssue; owner: string; repo: string }) { |
| 91 | const by = issue.author.display_username ?? issue.author.username; |
| 92 | return ( |
| 93 | <Row |
| 94 | leading={<IssueIcon issue={issue} />} |
| 95 | title={issue.title} |
| 96 | subtitle={`#${issue.number} · ${by} · ${ago(issue.updated_at)}${issue.comment_count ? ` · ${issue.comment_count} comments` : ""}`} |
| 97 | onPress={() => router.push({ pathname: "/code/[owner]/[repo]/issues/[number]", params: { owner, repo, number: String(issue.number) } })} |
| 98 | /> |
| 99 | ); |
| 100 | } |